<?php
/**
 * File defines the {@link E55ential5_Url_Parser} class.
 *
 * {@link E55ential5_Url_Parser} class is used to parse an incoming URL into
 * usable parts for dynamic processing.
 *
 * @author  Albert Fama <afama@albertfama.com>
 * @license http://www.gnu.org/licenses/gpl-3.0.txt 
 * @version 1.0 (Stable)
 * @package E55ential5
 */

/**
 * E55ential5_Url_Parser class used to parse an incoming URL into usable parts.
 *
 * E55ential5_Url_Parser implemented as a Singleton. To create an object of the 
 * class use {@link E55ential5_Url_Parser::getInstance()}. Class provides 
 * convient access to values retrieved from REQUEST_URI, after standardization.
 *
 * UnitTests found in: {@link E55ential5_UnitTests_Url_ParserTest}
 *
 * @author     Albert Fama <afama@albertfama.com>
 * @version    1.0 (Stable)
 * @package    E55ential5
 * @subpackage Url
 */
class E55ential5_Url_Parser
{
    /**
     * Static variable used to hold instance of E55ential5_Url_Parser.
     *
     * E55ential5_Url_Parser follows a standard Singleton implementation in 
     * PHP5. E55ential5_Url_Parser::_instance is a static variable used to hold
     * an instance of E55ential5_Url_Parser.
     *
     * @see E55ential5_Url_Parser::getInstance()
     * @var E55ential5_Url_Parser
     */
    private static $_instance;

    /**
     * The value of REQUEST_URI after standardization.
     *
     * @see E55ential5_Url_Parser::_setPathQuery
     * @var string
     */
    private $_path_query;

    /**
     * Associative array of URL variable names and values.
     *
     * Associative array created using the $query_vars sent to the 
     * E55ential5_Url_Parser::__constructor() as the element keys. Element 
     * values set to the incoming values from the requested URL.
     *
     * @see E55ential5_Url_Parser::_setQueryVars()
     * @see E55ential5_Url_Parser::__constructor()
     * @var array
     */
    private $_query_vars;

    /**
     * Remainder of URL after varables parsed.
     *
     * After the E55ential5_Url_Parser::_query_vars are set, this holds the 
     * remaining string from the URL.
     *
     * @see E55ential5_Url_Parser::_query_vars
     * @var string
     */
    private $_query_mod;

    /**
     * Private constructor of E55ential5_Url_Parser.
     *
     * Constructor calls E55ential5_Url_Parser::_setPathQuery(), inializes 
     * member variables and finally calls 
     * E55ential5_Url_Parser::setQueryVariables();
     *
     * The value of $query_vars is an array containing the names to associate
     * with the values found in the URL. For example the URL parsed is:
     * example.com/profile/edit and the $query_vars sent is:
     * array('module', 'action'). After parsing the 'module' will be set to
     * 'profile' and the 'action' set to 'edit'.
     *
     * The value of $site_path is set to the loacation of where the application
     * resides on the server relative to the DOCUMENT_ROOT. For example if this
     * class is used to control only the forums section of the site
     * 'example.com' and the forum section is located at
     * 'example.com/members/forum/', the $site_path would be '/members/forum/'.
     *
     * @see    E55ential5_Url_Parser::_setPathQuery()
     * @see    E55ential5_Url_Parser::_setQueryVariables()
     * @param  array  $query_vars
     * @param  string $site_path
     * @return E55ential5_Url_Parser
     */
    private function __construct(array $query_vars, $site_path)
    {
        $this->_setPathQuery($site_path);

        (string)$this->_query_mod;

        $this->setQueryVariables($query_vars);
    }

    /**
     * Used to retrieve an instance of E55ential5_Url_Parser.
     *
     * E55ential5_Url_Parser created as a Singleton. This method first checks 
     * if an instance of the class has previously been created, if so method 
     * returns the existing instance. If not an instance is created and saved 
     * to E55ential5_Url_Parser::_instance, then returned. 
     *
     * @see    E55ential5_Url_Parser::_instance
     * @see    E55ential5_Url_Parser::__construct()
     * @param  array  $query_vars
     * @param  string $site_path
     * @return E55ential5_Url_Parser
     */
    public function getInstance(array $query_vars, $site_path='')
    {
        if (NULL === self::_instance) {
            self::_instance = new E55ential5_Url_Parser($query_vars, 
                                                        $site_path);
        }

        return self::_instance;

    }

    /**
     * Removes site path from E55ential5_Url_Parser::_path_query.
     *
     * Called from E55ential5_Url_Parser::_setQueryPath(). Then checks to 
     * determine if site path is contained at the beginning of the path_query. 
     * If so, method removes the site path from the path query.
     *
     * @see    E55ential5_Url_Parser::_setQueryPath()
     * @see    E55ential5_Url_Parser::_path_query
     * @param  string $site_path (see constructor documentation)
     * @return void
     */
    private function _stripSitePath($site_path)
    {
        $site_path = trim($site_path, '/ ');

        //get initial path length
        $path_len = strlen($site_path);
        //check that we have an actual site path
        if (0 === $path_len) {
            return;
        }

        //site path normalization
        $site_path  = '/'.$site_path.'/';
        $path_len  += 2;

        //if path_query does not begin with site path nothing to do.
        if (0 !== strpos($this->_path_query, $site_path)) {
            return;
        }

        //remove site path from url
        $this->_path_query = substr($this->_path_query, $path_len);
    }

    /**
     * Sets E55ential5_Url_Parser::_path_query.
     *
     * Method first uses REQUEST_URI to set default 
     * E55ential5_Url_Parser::_path_query. Then calls 
     * E55ential5_Url_Parser::stripSitePath() to remove $site_path. Method then 
     * checks for any $_GET variables in the URL. If found, $_GET string is
     * stripped. Finally any slashes and whitespace is removed from beginning
     * or end of E55ential5_Url_Parser::_path_query. 
     *
     * @see    E55ential5_Url_Parser::_stripSitePath()
     * @see    E55ential5_Url_Parser::_path_query
     * @param  string $site_path (see constructor documentation)
     * @return void
     */
    private function _setPathQuery($site_path)
    {
        //default path query - REQUEST_URI
        $this->_path_query = $_SERVER['REQUEST_URI'];

        $this->_stripSitePath($site_path);

        //strip $_GET from URL if exists
        $q_mark = strpos($this->_path_query, '?');
        if (FALSE !== $q_mark) {
            $this->_path_query = substr($this->_path_query, 0, $q_mark);
        }

        //strip opening and closing '/' and whitespace from URL if exists
        $this->_path_query = trim($this->_path_query, '/ ');
    }

    /**
     * E55ential5_Url_Parser::_query_vars & E55ential5_Url_Parser::_query_mod
     *
     * Method's second argument $preserve, useful when this method is called 
     * multiple times during a request. On calls following the initial, if 
     * $preserve is set to TRUE the method will preserve the original $vars 
     * sent during the previous calls and add the new $vars sent to the end of 
     * the $vars array.
     *
     * Method breaks apart E55ential5_Url_Parser::_path_query. The first 
     * element of the $vars sent and the first value of the result array from 
     * the exploding the E55ential5_Url_Parser::_path_query is used as the 
     * value; the method continues to loop through the $vars (names) sent, 
     * grabbing the values from E55ential5_Url_Parser::_path_query.
     *
     * If all $vars have been set in E55ential5_Url_Parser::_query_vars, 
     * the remainder of E55ential5_Url_Parser::_path_query is stored in 
     * E55ential5_Url_Parser::_query_mod.
     *
     * @see    E55ential5_Url_Parser::_path_query
     * @see    E55ential5_Url_Parser::_query_vars
     * @see    E55ential5_Url_Parser::_query_mod
     * @param  array   $vars     (variable names of values stored in url query)
     * @param  boolean $preserve (keep existing query variables)
     * @return void
     */
    public function setQueryVariables(array $vars, $preserve=TRUE)
    {
        //if no query path exit
        if (0 === strlen(trim($this->_path_query))) {
            return;
        }

        //if we're preserving query vars add to incoming $vars
        if (TRUE === (bool)$preserve && 
            TRUE === is_array($this->_query_vars)) {

            $c_vars = array_keys($this->_query_vars);
            $vars   = array_merge($current_vars, $vars);
        }

        //initialize member variables
        $this->_query_vars = array();
        $this->_query_mod  = '';

        $count = count($vars);
        //explode path query to get variable values (explode limit set to grab
        //any remaining values after vars parsed)
        $url_vars = explode('/', $this->_path_query, ($count+1));

        //spin through vars to grab values
        for ($i=0; $i<$count; $i++) {
            //check that query var is set
            if (FALSE === isset($url_vars[$i])) {
                //no value for var set to null
                $this->_query_vars[$vars[$i]] = NULL;

                //if element does not exist there will be no url vars following
                break;
            }

            //save value to member variable
            $this->_query_vars[$vars[$i]] = $url_vars[$i];
        }

        //set the remainder of url to member variable
        if (TRUE === isset($url_vars[$count])) {
            $this->_query_mod = $url_vars[$count];
        }
    }

    /**
     * Returns value of E55ential5_Url_Parser::_path_query
     *
     * @see    E55ential5_Url_Parser::_path_query
     * @return string
     */
    public function getPathQuery()
    {
        return $this->_path_query;
    }

    /**
     * Returns value of query variable sent to method
     *
     * Used to retrieve values within url query.  Method first checks that the
     * $var_name sent is set as a key in the E55ential5_Url_Parser::_query_vars
     * array. If set, method returns value of $var_name, else method returns
     * NULL.
     *
     * @see    E55ential5_Url_Parser::_query_vars
     * @param  string $var_name
     * @return mixed
     */
    public function getQueryVar($var_name)
    {
        if (FALSE === isset($this->_query_vars[$var_name])) {
            return NULL;
        }

        return $this->_query_vars[$var_name];
    }

    /**
     * Returns value of E55ential5_Url_Parser::_query_mod
     *
     * @see    E55ential5_Url_Parser::_query_mod
     * @return string
     */
    public function getQueryMod()
    {
        return $this->_query_mod;
    }
}
